P08-2. 통합 수집기 Grafana Alloy와 Mimir VictoriaMetrics - 실습 검증

Part 08 (실습): LGTM 메트릭 파이프라인 — Grafana Alloy와 Mimir / VictoriaMetrics 검증

실습 목표

  1. 관측 전용 네임스페이스(monitoring)를 생성하고 Ambient Mesh(istio.io/dataplane-mode=ambient)에 편입시켜 STRICT mTLS 환경의 워크로드를 안전하게 스크래핑할 수 있도록 구성한다.
  2. Prometheus/Mimir 100% 호환 시계열 DB인 VictoriaMetrics (v1.154.1), 외부 PostgreSQL 18 VM을 감시하는 postgres-exporter (v0.16.0), 그리고 차세대 단일 통합 수집기인 Grafana Alloy (v1.21.2) 를 배포한다.
  3. VictoriaMetrics의 PromQL HTTP API(/api/v1/query)를 직접 호출하여 Waypoint Proxy의 L7 RED 메트릭(istio_requests_total, p99 지연 시간), ztunnel의 L4 TCP 연결 메트릭(istio_tcp_connections_opened_total), 그리고 외부 PostgreSQL 18 VM의 실시간 상태(pg_up, pg_locks_count, pg_stat_activity_count) 가 단일 TSDB에서 통합 조회되는 것을 100% 실측한다.

사전 환경 확인

  • P08-1 이론 개념을 먼저 읽고 오면 Grafana Alloy의 River 문법과 히스토그램 버킷(_bucket, le, p99) 공식이 한눈에 이해됩니다.
  • Windows PowerShell 창을 열고 아래 명령어로 클러스터 상태를 확인합니다.
    $env:Path += ";$HOME\bin"
    cd "$HOME\mesh-study"
    kubectl get nodes
    


실습 아키텍처 및 메트릭 수집 파이프라인 한눈에 보기

flowchart LR
    subgraph TARGETS["메트릭 수집 대상 (Scrape Targets)"]
        ZT["ztunnel (istio-system)
:15020/stats/prometheus
L4 TCP/HBONE 메트릭"] WP["Waypoint (part1-demo)
:15020/stats/prometheus
L7 HTTP RED & Histogram"] PG_EXP["postgres-exporter (part1-demo)
:9187/metrics
(외부 study-pg-vm:5432 감시)"] end subgraph ALLOY_PIPE["monitoring 네임스페이스 (Ambient Mesh 편입!)"] ALLOY["Grafana Alloy (v1.21.2)
1. discovery.kubernetes 'pods'
2. prometheus.scrape (5s 주기)
3. prometheus.remote_write"] VM["VictoriaMetrics (v1.154.1)
:8428 (/api/v1/write & /api/v1/query)
Mimir/Prometheus 100% 호환 TSDB"] end ZT -->|"5초 주기 Scrape"| ALLOY WP -->|"5초 주기 Scrape"| ALLOY PG_EXP -->|"HBONE mTLS Scrape"| ALLOY ALLOY ==>|"Remote Write"| VM

Step 1. monitoring 네임스페이스 생성 및 Ambient Mesh 편입 (STRICT mTLS 연동!)

앞서 Part 04에서 우리는 part1-demo 네임스페이스에 PeerAuthentication: strict-mtls (mode: STRICT) 정책을 걸어 두었습니다.
만약 monitoring 네임스페이스에 뜨는 수집기(Grafana Alloy)가 Ambient Mesh 바깥에서 평문(Plaintext)으로 part1-demo의 파드(postgres-exporter)를 긁으려 하면 ztunnel이 즉시 연결을 차단합니다!
따라서 monitoring 네임스페이스도 istio.io/dataplane-mode=ambient 라벨을 붙여 메시 가족으로 편입시킵니다.

# 1. monitoring 네임스페이스 생성
kubectl create namespace monitoring --dry-run=client -o yaml | kubectl apply -f -

# 2. monitoring 네임스페이스를 Ambient Mesh에 편입 (Alloy가 HBONE mTLS로 스크래핑 가능!)
kubectl label namespace monitoring istio.io/dataplane-mode=ambient --overwrite

Step 2. VictoriaMetrics · postgres-exporter · Grafana Alloy 통합 배포

이제 3개의 핵심 컴포넌트를 한 번에 배포합니다:

  1. vmsingle (victoriametrics/victoria-metrics:v1.154.1): 8428번 포트에서 메트릭을 저장하고 PromQL 쿼리에 응답하는 초고속 시계열 DB입니다.
  2. postgres-exporter (prometheuscommunity/postgres-exporter:v0.16.0): part1-demo 안에서 실행되며, 우리가 Part 06에서 만든 메시 도메인(pg-vm.mesh.internal:5432)을 타고 외부 PostgreSQL 18 VM에 접속해 DB 내부 지표(pg_locks_count, pg_stat_activity_count)를 Prometheus 포맷(:9187/metrics)으로 변환해 줍니다!
  3. alloy (grafana/alloy:v1.21.2): 클러스터 내 모든 15020 포트(ztunnel 및 Waypoint)와 postgres-exporter(9187 포트)를 5초마다 긁어서 vmsingle로 전송(remote_write)합니다.
실무 주의사항: Grafana Alloy에서 scrape_interval = "5s" 설정 시 scrape_timeout 필수!

prometheus.scrape 블록의 기본 scrape_timeout(응답 대기 제한 시간)은 10s 입니다. 만약 scrape_interval(수집 주기)만 "5s"로 줄이고 scrape_timeout을 안 적으면, Alloy가 기동할 때 Error: scrape_timeout (10s) greater than scrape_interval (5s) 에러를 내며 멈춥니다! 따라서 주기를 10초 미만으로 줄일 때는 반드시 scrape_timeout = "4s" 처럼 수집 주기보다 작은 값을 함께 명시해야 합니다.

# part8-metrics-stack.yaml 생성 및 적용
@"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vmsingle
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vmsingle
  template:
    metadata:
      labels:
        app: vmsingle
    spec:
      containers:
      - name: vmsingle
        image: victoriametrics/victoria-metrics:v1.154.1
        args:
        - "-retentionPeriod=7d"
        - "-httpListenAddr=:8428"
        ports:
        - containerPort: 8428
---
apiVersion: v1
kind: Service
metadata:
  name: vmsingle
  namespace: monitoring
spec:
  selector:
    app: vmsingle
  ports:
  - name: http
    port: 8428
    targetPort: 8428
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-exporter
  namespace: part1-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres-exporter
  template:
    metadata:
      labels:
        app: postgres-exporter
    spec:
      containers:
      - name: postgres-exporter
        image: prometheuscommunity/postgres-exporter:v0.16.0
        env:
        - name: DATA_SOURCE_NAME
          value: "postgresql://meshuser:meshpass@pg-vm.mesh.internal:5432/ordersdb?sslmode=disable"
        ports:
        - containerPort: 9187
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-exporter
  namespace: part1-demo
spec:
  selector:
    app: postgres-exporter
  ports:
  - name: metrics
    port: 9187
    targetPort: 9187
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: alloy
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: alloy-observer
rules:
- apiGroups: [""]
  resources: ["nodes", "nodes/proxy", "nodes/metrics", "services", "endpoints", "pods", "pods/log", "namespaces"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: alloy-observer
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: alloy-observer
subjects:
- kind: ServiceAccount
  name: alloy
  namespace: monitoring
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: alloy-config
  namespace: monitoring
data:
  config.alloy: |
    discovery.kubernetes "pods" {
      role = "pod"
    }

    // 1. Istio ztunnel & Waypoint (:15020/stats/prometheus) 자동 발견 및 수집
    discovery.relabel "istio_mesh" {
      targets = discovery.kubernetes.pods.targets
      rule {
        source_labels = ["__meta_kubernetes_pod_container_port_number"]
        regex         = "15020|15090"
        action        = "keep"
      }
      rule {
        source_labels = ["__meta_kubernetes_namespace"]
        target_label  = "namespace"
      }
      rule {
        source_labels = ["__meta_kubernetes_pod_name"]
        target_label  = "pod"
      }
    }

    prometheus.scrape "istio_mesh" {
      targets         = discovery.relabel.istio_mesh.output
      metrics_path    = "/stats/prometheus"
      scrape_interval = "5s"
      scrape_timeout  = "4s"
      forward_to      = [prometheus.remote_write.vmsingle.receiver]
    }

    // 2. 외부 PostgreSQL 18 VM 감시용 postgres-exporter (:9187/metrics) 수집
    prometheus.scrape "postgres_exporter" {
      targets = [{
        __address__ = "postgres-exporter.part1-demo.svc.cluster.local:9187",
        job         = "postgres-exporter",
        db_instance = "study-pg-vm",
      }]
      scrape_interval = "5s"
      scrape_timeout  = "4s"
      forward_to      = [prometheus.remote_write.vmsingle.receiver]
    }

    // 3. VictoriaMetrics (Mimir/Prometheus 호환 TSDB)로 Remote Write 전송
    prometheus.remote_write "vmsingle" {
      endpoint {
        url = "http://vmsingle.monitoring.svc.cluster.local:8428/api/v1/write"
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: alloy
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: alloy
  template:
    metadata:
      labels:
        app: alloy
    spec:
      serviceAccountName: alloy
      containers:
      - name: alloy
        image: grafana/alloy:v1.21.2
        args:
        - "run"
        - "/etc/alloy/config.alloy"
        - "--server.http.listen-addr=0.0.0.0:12345"
        ports:
        - containerPort: 12345
          name: http-ui
        volumeMounts:
        - name: config
          mountPath: /etc/alloy
      volumes:
      - name: config
        configMap:
          name: alloy-config
"@ | Set-Content -Path "$HOME\mesh-study\part8-metrics-stack.yaml" -Encoding utf8

kubectl apply -f "$HOME\mesh-study\part8-metrics-stack.yaml"
kubectl wait --for=condition=Ready pod --all -n monitoring --timeout=120s
kubectl wait --for=condition=Ready pod -l app=postgres-exporter -n part1-demo --timeout=120s

실제 출력 결과:

deployment.apps/vmsingle created
service/vmsingle created
deployment.apps/postgres-exporter created
service/postgres-exporter created
...
pod/alloy-5dcbd94545-kjhw5 condition met
pod/vmsingle-7b77cc877c-5ds97 condition met
pod/postgres-exporter-674db86bbc-vb267 condition met

Step 3. L7 Waypoint RED 메트릭 및 p99 지연 시간(Histogram) 실측 쿼리!

이제 study-client에서 order-api로 10번 요청을 보낸 뒤, VictoriaMetrics의 /api/v1/query 엔드포인트에 PromQL을 날려 지금까지 누적된 서비스별·응답코드별 요청 수(istio_requests_total) 와 p99 지연 시간(histogram_quantile) 을 조회합니다!

# 1. 테스트 트래픽 10회 발생 후 스크래핑 주기(5초) 대기
kubectl exec -n part1-demo study-client -- curl -s "http://order-api/metrics-test?[1-10]" | Out-Null
Start-Sleep -Seconds 6

# 2. [R & E] 서비스별 / HTTP 상태 코드별 총 요청 횟수(istio_requests_total) 조회!
kubectl exec -n part1-demo study-client -- curl -s "http://vmsingle.monitoring.svc.cluster.local:8428/api/v1/query?query=sum(istio_requests_total)+by+(destination_service_name,response_code)"

# 3. [D] 상위 99% 응답 지연 시간(p99 Latency, 단위: ms) 조회!
kubectl exec -n part1-demo study-client -- curl -s -g "http://vmsingle.monitoring.svc.cluster.local:8428/api/v1/query?query=histogram_quantile(0.99,sum(rate(istio_request_duration_milliseconds_bucket[5m]))by(le,destination_service_name))"

실제 출력 결과:

// [1. 서비스별 / 상태 코드별 요청 횟수]
{
  "status": "success",
  "data": {
    "resultType": "vector",
    "result": [
      {"metric": {"destination_service_name": "order-api", "response_code": "200"}, "value": [1792913696, "50"]},
      {"metric": {"destination_service_name": "order-api", "response_code": "403"}, "value": [1792913696, "6"]},
      {"metric": {"destination_service_name": "order-api-v1", "response_code": "200"}, "value": [1792913696, "60"]},
      {"metric": {"destination_service_name": "order-api-v1", "response_code": "500"}, "value": [1792913696, "4"]},
      {"metric": {"destination_service_name": "order-api-v1", "response_code": "503"}, "value": [1792913696, "12"]},
      {"metric": {"destination_service_name": "order-api-v2", "response_code": "200"}, "value": [1792913696, "20"]}
    ]
  }
}

// [2. order-api의 p99 지연 시간 = 약 9.93ms!]
{
  "status": "success",
  "data": {
    "resultType": "vector",
    "result": [
      {"metric": {"destination_service_name": "order-api"}, "value": [1792913697, "9.928571428571427"]}
    ]
  }
}
우리가 앞선 파트에서 실험했던 모든 역사가 메트릭에 그대로 기록되어 있습니다!

  • Part 04에서 차단했던 해커 요청: destination_service_name="order-api", response_code="403" (6건)
  • Part 05에서 실험했던 카나리 배포(v1 vs v2)와 결함 주입(500), 서킷 브레이커(503): order-api-v1의 500(4건), 503(12건), 그리고 order-api-v2의 200(20건)까지 Waypoint Proxy가 하나도 놓치지 않고 측정해 VictoriaMetrics에 저장했습니다!
  • p99 지연 시간: order-api 요청 중 가장 느린 상위 1%(0.99)의 응답 시간이 9.93ms 임을 histogram_quantile 공식으로 정확히 산출해 냈습니다!


Step 4. L4 ztunnel TCP 메트릭 및 외부 PostgreSQL 18 VM 메트릭 통합 조회!

이번에는 HTTP가 아닌 순수 TCP 연결(외부 DB VM인 pg-vm.mesh.internal:5432 포함) 에 대한 L4 메트릭과, postgres-exporter가 수집한 외부 PostgreSQL 18 내부의 Lock 및 활성 세션 메트릭을 조회합니다.

# 1. [L4 Mesh] 목적지 서비스별 총 TCP 연결 오픈 횟수 (외부 DB VM인 pg-vm.mesh.internal 포함!)
kubectl exec -n part1-demo study-client -- curl -s "http://vmsingle.monitoring.svc.cluster.local:8428/api/v1/query?query=sum(istio_tcp_connections_opened_total)+by+(destination_service_name)"

# 2. [External DB] 외부 PostgreSQL 18 VM 생존 여부(pg_up = 1) 및 DB별 Lock 개수(pg_locks_count) 조회!
kubectl exec -n part1-demo study-client -- curl -s "http://vmsingle.monitoring.svc.cluster.local:8428/api/v1/query?query=pg_up"
kubectl exec -n part1-demo study-client -- curl -s "http://vmsingle.monitoring.svc.cluster.local:8428/api/v1/query?query=pg_locks_count" | Select-Object -First 1

실제 출력 결과:

// [1. L4 TCP 연결 오픈 횟수 — 외부 VM 도메인(pg-vm.mesh.internal: 32회)까지 완벽 추적!]
{
  "status": "success",
  "data": {
    "result": [
      {"metric": {"destination_service_name": "external-pg"}, "value": [1792913696, "6"]},
      {"metric": {"destination_service_name": "external-pg-se"}, "value": [1792913696, "16"]},
      {"metric": {"destination_service_name": "order-api"}, "value": [1792913696, "39"]},
      {"metric": {"destination_service_name": "pg-vm.mesh.internal"}, "value": [1792913696, "32"]}
    ]
  }
}

// [2. 외부 PostgreSQL 18 VM (db_instance="study-pg-vm") 생존 확인 및 Lock 모드별 카운트!]
{
  "status": "success",
  "data": {
    "result": [
      {"metric": {"__name__": "pg_up", "db_instance": "study-pg-vm", "job": "postgres-exporter"}, "value": [1792913752, "1"]}
    ]
  }
}

Step 5. 자주 발생하는 문제와 해결 (Troubleshooting)

증상 / 에러 메시지 원인 해결 방법
alloy 파드 로그에 scrape_timeout (10s) greater than scrape_interval (5s) 에러 출력 prometheus.scrape 블록의 기본 scrape_timeout이 10s여서 scrape_interval을 5s로 줄이면 역전 발생 scrape_interval = "5s" 밑에 반드시 scrape_timeout = "4s" 를 함께 지정
istio_requests_total은 조회되는데 pg_up이 빈 배열([])로 나옴 part1-demo에 PeerAuthentication(STRICT)가 걸려 있는데 monitoring 네임스페이스가 Ambient Mesh에 편입되지 않아 평문 스크래핑이 차단됨 kubectl label namespace monitoring istio.io/dataplane-mode=ambient --overwrite 실행
curl로 [5m]이 들어간 PromQL 호출 시 curl: (3) bad range in URL 에러 발생 curl이 대괄호([5m])를 URL 범위 확장 문법([1-10])으로 오인함 curl에 -g (--globoff) 옵션을 붙여서 실행 (curl -s -g "http://...")

Step 6. 이번 파트 핵심 명령어 정리 (Command Reference)

# 1. Grafana Alloy 수집기 상태 및 파이프라인 로드 로그 확인
kubectl logs -n monitoring -l app=alloy --tail=20

# 2. VictoriaMetrics에서 L7 HTTP 요청 수(Rate/Error) 조회
kubectl exec -n part1-demo study-client -- curl -s "http://vmsingle.monitoring.svc.cluster.local:8428/api/v1/query?query=sum(istio_requests_total)+by+(destination_service_name,response_code)"

# 3. VictoriaMetrics에서 상위 99% 지연 시간(p99 Duration) 조회 (-g 옵션 필수)
kubectl exec -n part1-demo study-client -- curl -s -g "http://vmsingle.monitoring.svc.cluster.local:8428/api/v1/query?query=histogram_quantile(0.99,sum(rate(istio_request_duration_milliseconds_bucket[5m]))by(le,destination_service_name))"

Step 7. 셀프 체크 (Self-Check Q&A)

Q1. 마이크로서비스의 응답 속도를 감시할 때 왜 '평균(Average)' 대신 p99 백분위수(histogram_quantile(0.99, ...))를 사용해야 하나요?

Q2. part1-demo 네임스페이스에 STRICT mTLS 보안이 적용된 상태에서, monitoring 네임스페이스의 Grafana Alloy가 part1-demo 안의 postgres-exporter를 정상적으로 스크래핑하려면 어떤 조치가 필요한가요?


🔗 다음 파트로 이동

LGTM의 첫 번째 기둥인 M(Metric) 파이프라인이 완성되었습니다!
다음 Part 09에서는 두 번째 기둥인 L(Grafana Loki) 을 구축하여 ztunnel의 L4 HBONE 액세스 로그와 Waypoint Proxy의 L7 Envoy 액세스 로그를 실시간 수집하고, LogQL 로 서킷 브레이커(UO)와 보안 차단 로그를 정밀 타격해 봅니다!


네비게이션

이전 목록 다음
◀ Part 08 이론 목록으로 Part 09 이론 ▶